"""
    xfree.utils.helpers
    ~~~~~~~~~~~~~~~~~~~~~
    文件描述：助手函数
    作者：xfree
    邮箱：cy_83k@qq.com
"""
import ast
import os
import re
from email import message_from_string
import time
from functools import wraps

import pkg_resources
import unidecode
from babel import get_locale_identifier
from babel.dates import format_date as babel_format_date
from babel.dates import format_datetime as babel_format_datetime
from babel.dates import format_time as babel_format_time
from babel.dates import format_timedelta as babel_format_timedelta
from flask import current_app, flash, g, redirect, request, session, url_for
from flask_allows import Permission
from flask_babelplus import lazy_gettext as _
from flask_login import current_user
from markupsafe import Markup
from pytz import UTC
from werkzeug.local import LocalProxy
from werkzeug.utils import import_string, ImportStringError
from flask_themes2 import get_themes_list, render_theme_template
from xfree.extensions import babel, redis_store
from xfree.utils.http import is_safe_url
from xfree.utils.settings import xfree_config
import requests
from PIL import ImageFile

def string_to_bytes(text, encoding="utf-8"):
    """将字符串转换为字节
    ：param text: 字符串
    ：param encoding: 编码方式
    :return: 转化后的字节
    """
    if isinstance(text, str):
        text = text.encode(encoding)
    return text


def bytes_to_string(input_bytes, encoding="utf-8"):
    """将字节转换为字符串
    :param input_bytes:
    :param encoding:
    :return:
    """
    if not isinstance(input_bytes, str):
        input_bytes = input_bytes.decode(encoding)
    return input_bytes


def slugify(text, delim=u"-"):
    """将输入的文本转换为ASCII字符串
    用途： 在Web开发中，将文章标题转换为文件名
    :param text:
    :param delim:
    :return:
    """
    _punct_re = re.compile(r'[\t !"#$%&\'()*\-/<=>?@\[\\\]^_`{|},.]+')
    text = unidecode.unidecode(text)
    result = []
    for word in _punct_re.split(text.lower()):
        if word:
            result.append(word)
    return str(delim.join(result))


def redirect_url(endpoint, use_referrer=True):
    """根据是否使用了 referrer（即请求头中的 Referer 字段），决定生成一个重定向URL
    用途： 这段代码通常用于在用户登录后，重定向用户返回到之前的页面，或者在用户未登录的情况下重定向到登录页面
    ：param endpoint: 目标URL
    ：param use_referrer: 是否使用 referrer
    ：return: 重定向URL
    """
    targets = [endpoint]
    allowed_hosts = current_app.config["ALLOWED_HOSTS"]
    if use_referrer:
        targets.insert(0, request.referrer)
    for target in targets:
        if target and is_safe_url(target, allowed_hosts):
            return target


def redirect_or_next(endpoint, use_referrer=True):
    """将用户重定向回他们正在查看的页面或指定的端点,这是对Flask的:func:`Flask.redirect`函数的封装。
    :param endpoint: 回退终结点
    :param use_referrer: 是否使用 referrer
    :return:
    """
    # 首先尝试获取请求参数中的"next"值作为重定向地址
    # 使用redirect_url函数处理"next"值，并根据use_referrer参数决定是否使用引用地址
    # 如果得到的重定向地址存在，则使用该地址进行重定向
    # 如果不存在，则使用redirect_url函数处理endpoint作为重定向地址
    return redirect(
        redirect_url(request.args.get("next"), use_referrer)
        or redirect_url(endpoint, use_referrer)
    )


def get_xfree_config(app, config_file):
    """从配置文件中获取XFree配置
    :param app: Flask应用程序实例
    :param config_file: 配置文件名
    :return: 配置文件内容
    """
    if config_file is not None:
        # 检查config_file是否为字符串
        if not isinstance(config_file, str):
            return config_file

        # 检查一个文件是否存在。如果存在，则返回文件的路径。
        if os.path.exists(os.path.join(app.instance_path, config_file)):
            return os.path.join(app.instance_path, config_file)

        # 检查给定的配置文件路径config_file是否存在。如果存在，则返回该文件的绝对路径。用途是确保返回的路径是绝对路径，而不是相对路径。这样可以避免在不同环境（如不同操作系统或目录结构）下的混淆。
        if os.path.exists(os.path.abspath(config_file)):
            return os.path.join(os.path.abspath(config_file))

        # 从配置文件中导入一个模块或函数，如果导入失败，则返回None。
        try:
            return import_string(config_file)
        except ImportStringError:
            return None
    else:
        # 拼接XFree项目的配置xfree.cfg目录
        project_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
        project_config = os.path.join(project_dir, "xfree","configs","xfree.cfg")

        # 拼接flask实例配置路径xfree.cfg
        instance_config = os.path.join(app.instance_path, "xfree","configs","xfree.cfg")

        # 实例配置文件存在直接返回
        if os.path.exists(instance_config):
            return instance_config
        # 实例配置文件不存在,项目配置文件存在直接返回
        if os.path.exists(project_config):
            return project_config


def app_config_from_env(app, prefix="XFREE_"):
    """从环境变量中读取配置信息，并将其添加到Flask应用程序的配置中
    ：param app: Flask应用程序实例
    ：param prefix: 环境变量前缀
    """
    for key, value in os.environ.items():
        if key.startswith(prefix):
            key = key[len(prefix):]
            try:
                value = ast.literal_eval(value)
            except (ValueError, SyntaxError):
                pass
            app.config[key] = value
    return app


def parse_pkg_metadata(dist_name):
    """解析包的元数据
    ：param dist_name: 包名
    ：return: 包的元数据
    """
    try:
        raw_metadata = pkg_resources.get_distribution(dist_name).get_metadata(
            "METADATA"
        )
    except FileNotFoundError:
        raw_metadata = pkg_resources.get_distribution(dist_name).get_metadata(
            "PKG-INFO"
        )

    metadata = {}

    # 解析元数据
    for key, value in message_from_string(raw_metadata).items():
        metadata[key.replace("-", "_").lower()] = value

    return metadata


def time_utcnow():
    """获取UTC时间"""
    return datetime.now(UTC)



def get_available_languages():
    """
    返回包含所有可用语言的列表。中的项目列表是元组，其中元组的第一项是语言环境标识符（即 de_AT）和第二个显示区域设置的名称。
    ：return: 语言列表
    例如：
        [('de_AT', 'Deutsch (Österreich)')]
    """
    return [
        (get_locale_identifier((l.language, l.territory)), l.display_name)
        for l in babel.list_translations()
    ]


def get_available_themes():
    """返回包含所有可用主题的列表。中的项目列表是元组，其中元组的第一项是标识符和第二个是主题的名称。
    ：return: 主题列表
    例如：

        [('aurora_mod', 'Aurora Mod')]
    """
    return [(theme.identifier, theme.name) for theme in get_themes_list()]


import itertools
import operator


def get_categories_and_forums(query_result, user):
    """
    返回一个包含类别和其关联论坛列表的列表。

    结构如下：
    [(<类别 1>, [(<论坛 1>, None), (<论坛 2>, <特定对象>)]), (<类别 2>, [(<论坛 3>, None), (<论坛 4>, None)])]

    解包此结构的方式如下：
    for category, forums in x:
        print(category)
        for forum, forumsread in forums:
            print("\t", forum, forumsread)

    这将打印类似如下内容：
    <类别 1>
        <论坛 1> None
        <论坛 2> <特定对象>
    <类别 2>
        <论坛 3> None
        <论坛 4> None

    参数：
    query_result：一个包含所有类别和论坛信息的元组（可能是 KeyedTuple 类型）。
    user：用户对象，用于判断用户是否已登录，因为未登录用户没有论坛阅读关系的关联。
    """
    # 使用 itertools.groupby 函数按照类别对查询结果进行分组
    # operator.itemgetter(0) 作为分组的依据，假设查询结果的第一个元素是类别标识
    it = itertools.groupby(query_result, operator.itemgetter(0))

    forums = []

    # 如果用户已登录
    if user.is_authenticated:
        for key, value in it:
            # 对于每个类别，将其对应的论坛和论坛阅读状态对象组成元组添加到列表中
            forums.append((key, [(item[1], item[2]) for item in value]))
    # 如果用户未登录
    else:
        for key, value in it:
            # 对于每个类别，将其对应的论坛和 None 组成元组添加到列表中
            forums.append((key, [(item[1], None) for item in value]))

    return forums


import itertools
import operator


def get_forums(query_result, user):
    """
    返回一个包含类别和论坛列表的元组。

    这是与 get_categories_and_forums 相对应的函数，特别适用于只需要一个类别的论坛的情况。

    例如：
    (<类别 2>, [(<论坛 3>, None), (<论坛 4>, None)])

    参数：
    query_result：一个包含所有类别和论坛信息的元组（可能是 KeyedTuple 类型）。
    user：用户对象，用于判断用户是否已登录，因为未登录用户没有论坛阅读关系的关联。
    """
    # 使用 itertools.groupby 函数按照类别对查询结果进行分组
    # operator.itemgetter(0) 作为分组的依据，假设查询结果的第一个元素是类别标识
    it = itertools.groupby(query_result, operator.itemgetter(0))

    if user.is_authenticated:
        # 如果用户已登录，遍历分组结果，将类别和对应的论坛及论坛阅读状态对象组成元组
        for key, value in it:
            forums = key, [(item[1], item[2]) for item in value]
    else:
        # 如果用户未登录，遍历分组结果，将类别和对应的论坛及 None 组成元组
        for key, value in it:
            forums = key, [(item[1], None) for item in value]

    return forums


from datetime import datetime, timedelta


def topic_is_unread(topic, topicsread, user, forumsread=None):
    """
    检查一个主题是否未读。

    参数：
    topic：要检查是否未读的主题。
    topicsread：该主题的已读对象。
    user：要检查其是否已阅读主题中最后一个帖子的用户。
    forumsread：主题所在的论坛已读对象。如果还想检查用户是否已将论坛标记为已读，那么需要传入一个 forumsread 对象。
    """
    # 如果用户未登录，直接返回 False
    if not user.is_authenticated:
        return False

    # 获取当前 UTC 时间减去追踪时长后的时间
    read_cutoff = datetime.utcnow() - timedelta(days=xfree_config["TRACKER_LENGTH"])

    # 如果追踪时长设置为 0，则禁用追踪器，直接返回 False
    if xfree_config["TRACKER_LENGTH"] == 0:
        return False

    # 检查主题的最后更新时间是否早于追踪截止时间
    if topic.last_updated < read_cutoff:
        return False

    # 如果主题已读对象为 None，表示用户要么将论坛标记为已读，要么还未访问过该主题
    if topicsread is None:
        # 用户已清除论坛 - 检查是否有新帖子
        if forumsread and forumsread.cleared is not None:
            return forumsread.cleared < topic.last_updated

        # 用户还未阅读该主题，或者自用户将论坛标记为已读后有新帖子
        return True

    # 检查自用户上次访问主题后是否有新帖子
    return topicsread.last_read < topic.last_updated

def check_image(url):
    """
    这是对 :func:`get_image_info` 函数的一个小封装。
    如果图像不符合 `xfree_config` 设置，它将返回一个元组，第一个值是自定义错误消息，第二个值为 `False`，表示未通过检查。
    如果检查成功，它将返回 `None` 作为错误消息，`True` 表示通过检查。

    参数：
    url：要检查的图像 URL。
    """
    img_info = get_image_info(url)
    error = None

    if img_info is None:
        error = "Couldn't get image info. Try a different hoster and/or image."
        return error, False

    # 检查图像大小
    if img_info["size"] > xfree_config["AVATAR_SIZE"]:
        error = "Image is too big! {}kb are allowed.".format(
            xfree_config["AVATAR_SIZE"]
        )
        return error, False

    # 检查图像类型
    if not img_info["content_type"] in xfree_config["AVATAR_TYPES"]:
        error = "Image type {} is not allowed. Allowed types are: {}".format(
            img_info["content_type"], ", ".join(xfree_config["AVATAR_TYPES"])
        )
        return error, False

    # 检查图像宽度
    if img_info["width"] > xfree_config["AVATAR_WIDTH"]:
        error = "Image is too wide! {}px width is allowed.".format(
            xfree_config["AVATAR_WIDTH"]
        )
        return error, False

    # 检查图像高度
    if img_info["height"] > xfree_config["AVATAR_HEIGHT"]:
        error = "Image is too high! {}px height is allowed.".format(
            xfree_config["AVATAR_HEIGHT"]
        )
        return error, False

    return error, True




def get_image_info(url):
    """
    返回图像的内容类型、大小（以千字节为单位）、高度和宽度，而无需完全下载图像。

    参数：
    url：图像的 URL。
    """

    try:
        # 发送 GET 请求获取图像，设置超时时间
        r = requests.get(url, timeout=(3.05, 27), stream=True)
    except requests.ConnectionError:
        # 如果连接错误，返回 None
        return None

    # 获取图像大小（字节数）
    image_size = r.headers.get("content-length", None)
    if image_size is None:
        # 如果没有获取到大小，返回 None
        return None

    # 将字节数转换为千字节
    image_size = float(image_size) / 1000
    # 设置最大图像大小为 10000KB
    image_max_size = 10000
    # 创建一个包含图像信息的字典
    image_data = {
        "content_type": "",
        "size": image_size,
        "width": 0,
        "height": 0,
    }

    # 设置硬限制为 10MB
    if image_size > image_max_size:
        return image_data

    data = None
    # 创建一个图像解析器
    parser = ImageFile.Parser()

    while True:
        data = r.raw.read(1024)
        if not data:
            break

        parser.feed(data)
        if parser.image:
            # 设置图像内容类型、宽度和高度
            image_data["content_type"] = parser.image.format
            image_data["width"] = parser.image.size[0]
            image_data["height"] = parser.image.size[1]
            break

    return image_data


#####模板过滤器函数#####
def crop_title(title, length=None, suffix="..."):
    """
    裁剪标题到指定长度。

    :param title: 要被裁剪的标题。
    :param length: 裁剪后的标题长度，若为 None，则使用 xfree_config["TITLE_LENGTH"] 的值。
    :param suffix: 标题被裁剪后添加在末尾的后缀。
    """
    # 如果未传入长度参数，则使用 xfree_config["TITLE_LENGTH"] 的值
    length = xfree_config["TITLE_LENGTH"] if length is None else length

    # 如果标题长度小于等于指定长度，直接返回原标题
    if len(title) <= length:
        return title

    # 对标题进行裁剪，取长度范围内最后一个空格前的部分，再加上后缀
    return title[:length].rsplit(" ", 1)[0] + suffix

def format_date(datetime):
    """
    格式化日期时间对象，在模板中仅保留日期部分。

    :param datetime: 要被格式化的日期时间对象。
    :return: Markup 对象（可能是经过安全处理的 HTML 标记）。
    """
    return _format_html_time_tag(datetime, "date-only")

def _format_html_time_tag(datetime, what_to_display):
    """
    格式化 HTML 的时间标签。

    :param datetime: 日期时间对象。
    :param what_to_display: 要显示的内容类型，可选值为"date-only"（仅日期）、"time-only"（仅时间）、"date-and-time"（日期和时间）。
    :return: Markup 对象，包含格式化后的时间标签。
    """
    if what_to_display == "date-only":
        # 如果要显示仅日期，使用 babel_format_date 格式化日期，根据用户的本地化设置进行格式化
        content = babel_format_date(datetime, locale=_get_user_locale())
    elif what_to_display == "time-only":
        # 如果要显示仅时间，使用 babel_format_time 格式化时间，格式为短格式，根据用户的本地化设置进行格式化
        content = babel_format_time(datetime, format="short", locale=_get_user_locale())
    elif what_to_display == "date-and-time":
        # 如果要显示日期和时间，使用 babel_format_datetime 格式化日期时间，时区设为 UTC，根据用户的本地化设置进行格式化
        content = babel_format_datetime(datetime, tzinfo=UTC, locale=_get_user_locale())
        # 虽然这可以通过格式字符串来完成，但这会阻碍国际化，而且说实话，何必呢。
        content += " UTC"
    else:
        # 如果传入的 what_to_display 参数无效，抛出 ValueError 异常
        raise ValueError("what_to_display argument invalid")

    isoformat = datetime.isoformat()

    # 返回一个 Markup 对象，包含格式化后的时间标签，其中包含日期时间的 ISO 格式、要显示的内容类型和格式化后的内容
    return Markup(
        '<time datetime="{}" data-what_to_display="{}">{}</time>'.format(
            isoformat, what_to_display, content
        )
    )

def _get_user_locale():
    """
    获取用户的本地化设置。

    :return: 用户的本地化设置字符串，默认为'en'（英语）。如果用户已登录且有指定的语言设置，则使用用户的语言设置。
    """
    # 获取配置中的默认语言，如果没有配置默认语言，则默认为'en'
    locale = xfree_config.get("DEFAULT_LANGUAGE", "en")
    # 如果当前用户已登录且用户有指定的语言设置
    if current_user.is_authenticated and current_user.language is not None:
        # 使用用户指定的语言设置
        locale = current_user.language
    return locale

def format_time(datetime):
    """
    格式化日期时间对象，在模板中仅保留时间部分。

    :param datetime: 要被格式化的日期时间对象。
    :return: Markup 对象（可能是经过安全处理的 HTML 标记）。
    """
    return _format_html_time_tag(datetime, "time-only")


def format_datetime(datetime):
    """
    格式化日期时间对象，用于在模板中显示。

    :param datetime: 要被格式化的日期时间对象。
    :return: Markup 对象（可能是经过安全处理的 HTML 标记），包含格式化后的日期和时间以及时区信息。
    """
    return _format_html_time_tag(datetime, "date-and-time")


def forum_is_unread(forum, forumsread, user):
    """
    检查一个论坛是否未读。

    :param forum: 要检查是否未读的论坛对象。
    :param forumsread: 针对该论坛的已读记录对象。
    :param user: 要检查其阅读状态的用户对象。
    :return: 如果论坛未读则返回 True，否则返回 False。
    """
    # 如果用户未登录，每个论坛都被标记为已读
    if not user.is_authenticated:
        return False

    # 获取当前 UTC 时间减去追踪时长的时间点
    read_cutoff = time_utcnow() - timedelta(days=xfree_config["TRACKER_LENGTH"])

    # 如果追踪时长设置为 0，则禁用追踪器，返回 False
    if xfree_config["TRACKER_LENGTH"] == 0:
        return False

    # 如果论坛中没有主题，标记为已读
    if forum and forum.topic_count == 0:
        return False

    # 检查论坛的最后一个帖子是否比追踪时长更新
    if forum.last_post_id is None or forum.last_post_created < read_cutoff:
        return False

    # 如果用户没有访问过论坛中的主题（即 forumsread 为 None），需要检查论坛是否仍未读
    if forum and not forumsread:
        return forum.last_post_created > read_cutoff

    try:
        # 检查论坛是否已被清除且自清除后是否有新帖子
        if forum.last_post_created > forumsread.cleared:
            # 如果最后一个帖子的创建时间早于用户上次阅读时间，则标记为已读
            if forum.last_post_created < forumsread.last_read:
                return False
    except TypeError:
        pass

    # 否则，只检查用户是否已阅读最后一个帖子
    return forum.last_post_created > forumsread.last_read

def is_online(user):
    """
    一个简单的检查，判断用户是否在指定时间范围内在线。

    :param user: 要检查的用户对象。
    :return: 如果用户在指定时间范围内在线则返回 True，否则返回 False。
    """
    return user.lastseen >= time_diff()


def time_diff():
    """
    计算当前时间与配置中的 ONLINE_LAST_MINUTES 变量所代表的时间之间的时间差。

    :return: 返回一个表示过去特定分钟数之前的时间点的时间对象。
    """
    # 获取当前 UTC 时间
    now = time_utcnow()
    # 计算当前时间减去 ONLINE_LAST_MINUTES 分钟后的时间点
    diff = now - timedelta(minutes=xfree_config["ONLINE_LAST_MINUTES"])
    return diff


def time_since(time):  # pragma: no cover
    """
    返回一个表示从当前时间到给定时间的时间间隔的字符串，例如“3 天前”“5 小时前”。

    :param time: 一个 datetime 对象，表示要计算时间间隔的目标时间。
    :return: 表示时间间隔的字符串。
    """
    # 计算从当前时间到给定时间的时间差
    delta = time - time_utcnow()
    # 格式化时间差为可读的字符串，并添加时间方向（如“前”或“后”）
    return format_timedelta(delta, add_direction=True)


def format_timedelta(delta, **kwargs):
    """
    对 babel 的 format_timedelta 函数进行封装，使其能够根据用户语言进行格式化。

    :param delta: 时间差对象。
    :param kwargs: 传递给 babel_format_timedelta 的其他参数。
    :return: 根据用户语言格式化后的时间差字符串。
    """
    return babel_format_timedelta(delta, locale=_get_user_locale(), **kwargs)


def topic_is_unread(topic, topicsread, user, forumsread=None):
    """
    检查一个主题是否未读。

    :param topic: 要检查是否未读的主题对象。
    :param topicsread: 针对该主题的已读记录对象。
    :param user: 要检查其阅读状态的用户对象。
    :param forumsread: 该主题所在的论坛的已读记录对象，如果要同时检查用户是否将论坛标记为已读，则需要传入这个参数。
    :return: 如果主题未读则返回 True，否则返回 False。
    """
    # 如果用户未登录，直接返回 False
    if not user.is_authenticated:
        return False

    # 获取当前 UTC 时间减去追踪时长的时间点
    read_cutoff = time_utcnow() - timedelta(days=xfree_config["TRACKER_LENGTH"])

    # 如果追踪时长设置为 0，则禁用追踪器，返回 False
    if xfree_config["TRACKER_LENGTH"] == 0:
        return False

    # 检查主题的最后更新时间是否早于追踪截止时间
    if topic.last_updated < read_cutoff:
        return False

    # 如果主题的已读记录为 None，表示用户可能将论坛标记为已读或者还未访问过该主题
    if topicsread is None:
        # 如果传入了论坛的已读记录对象，并且用户清除了论坛的已读状态，检查是否有新帖子
        if forumsread and forumsread.cleared is not None:
            return forumsread.cleared < topic.last_updated
        # 用户还未阅读过该主题，或者自用户将论坛标记为已读后有新帖子
        return True

    # 检查是否有新帖子自用户上次阅读该主题后
    return topicsread.last_read < topic.last_updated



def mark_online(user_id, guest=False):  # pragma: no cover
    """
    将用户标记为在线状态。

    :param user_id: 要标记为在线的用户的 ID。
    :param guest: 如果设置为 True，将用户添加到访客活动中，而不是用户活动中。
    :return: 无返回值，通过 Redis 操作将用户标记为在线状态。

    参考: http://flask.pocoo.org/snippets/71/
    """
    # 将用户 ID 转换为字节类型
    user_id = to_bytes(user_id)
    # 获取当前时间戳（秒）
    now = int(time.time())
    # 计算过期时间戳
    expires = now + (xfree_config["ONLINE_LAST_MINUTES"] * 60) + 10
    if guest:
        # 如果是访客，设置对应的 Redis 键
        all_users_key = "online-guests/%d" % (now // 60)
        user_key = "guest-activity/%s" % user_id
    else:
        # 如果是注册用户，设置对应的 Redis 键
        all_users_key = "online-users/%d" % (now // 60)
        user_key = "user-activity/%s" % user_id
    # 创建 Redis 管道操作
    p = redis_store.pipeline()
    # 将用户 ID 添加到在线用户集合中
    p.sadd(all_users_key, user_id)
    # 设置用户活动时间戳
    p.set(user_key, now)
    # 设置在线用户集合的过期时间
    p.expireat(all_users_key, expires)
    # 设置用户活动键的过期时间
    p.expireat(user_key, expires)
    # 执行管道中的所有操作
    p.execute()


def to_bytes(text, encoding="utf-8"):
    """
    将字符串转换为字节类型。

    :param text: 要转换的文本，可以是字符串或已经是字节类型的数据。
    :param encoding: 编码方式，默认为'utf-8'。
    :return: 如果输入是字符串，则返回编码后的字节类型数据；如果输入已经是字节类型，则直接返回。
    """
    if isinstance(text, str):
        text = text.encode(encoding)
    return text


def render_template(template, **context):  # pragma: no cover
    """
    一个辅助函数，使用`render_theme_template`函数，无需在所有视图中进行编辑。

    :param template: 要渲染的模板名称。
    :param context: 传递给模板的上下文变量。
    :return: 渲染后的模板内容。
    """
    if current_user.is_authenticated and current_user.theme:
        # 如果用户已认证且有设置主题，则使用用户设置的主题
        theme = current_user.theme
    else:
        # 否则，从会话中获取主题或使用默认主题
        theme = session.get("theme", xfree_config["DEFAULT_THEME"])
    return render_theme_template(theme, template, **context)



def get_alembic_locations(plugin_dirs):
    """
    返回一个由（分支名称，插件目录）组合构成的元组列表。
    分支名称是插件目录的名称，也应该是插件的唯一标识符。

    :param plugin_dirs: 插件目录的列表。
    :return: 包含分支名称和插件目录组合的元组列表。
    """
    # 使用列表推导式生成包含分支名称和插件目录的元组列表
    branches_dirs = [
        tuple([os.path.basename(os.path.dirname(p)), p]) for p in plugin_dirs
    ]

    return branches_dirs


class FlashAndRedirect(object):
    """
     一个辅助类，用于在重定向之前显示消息。
     使用方法：FlashAndRedirect(message, level, endpoint)
     """
    def __init__(self, message, level, endpoint):
        # 如果endpoint不可调用
        if not callable(endpoint):
            # 创建一个匿名函数endpoint_，该函数在调用时会调用url_for来生成对应的URL
            # 这里忽略传入匿名函数的参数（*a和**k），这种处理可能在需要添加查询参数时不完全正确
            endpoint_ = lambda *a, **k: url_for(endpoint)
        else:
            # 如果endpoint是可调用的，直接将endpoint赋值给endpoint_
            endpoint_ = endpoint

        # 保存消息内容
        self._message = message
        # 保存消息级别
        self._level = level
        # 保存处理后的重定向目标
        self._endpoint = endpoint_

    def __call__(self, *a, **k):
        # 使用flash函数传递消息和级别
        flash(self._message, self._level)
        # 使用redirect函数重定向到通过self._endpoint计算得到的URL
        return redirect(self._endpoint(*a, **k))


def get_online_users(guest=False):  # pragma: no cover
    """
    获取在指定时间范围内的所有在线用户

    :param guest: 如果为True，将返回在线访客；如果为False，将返回在线注册用户
    """
    # 获取当前时间（以分钟为单位）
    current = int(time.time()) // 60
    # 生成一个时间范围的分钟数序列
    minutes = range(xfree_config["ONLINE_LAST_MINUTES"])
    if guest:
        # 如果需要获取在线访客
        # 从Redis存储中获取在指定时间范围内的在线访客集合的并集
        # 构造在线访客在Redis中的键名列表，并使用sunion操作获取并集
        users = redis_store.sunion(
            ["online - guests/%d" % (current - x) for x in minutes]
        )
    else:
        # 如果需要获取在线注册用户
        # 从Redis存储中获取在指定时间范围内的在线注册用户集合的并集
        # 构造在线注册用户在Redis中的键名列表，并使用sunion操作获取并集
        users = redis_store.sunion(["online - users/%d" % (current - x) for x in minutes])

    # 将获取到的用户ID转换为Unicode字符串并返回
    return [to_unicode(u) for u in users]

def to_unicode(input_bytes, encoding="utf-8"):
    """
    将输入的字节串转换为文本（如果需要的话）。

    :param input_bytes: 需要处理的数据，可以是字节串或者字符串。
    :param encoding: 解码字节串时使用的字符编码，默认是"utf-8"。
    """
    # 判断输入的数据是否为字符串类型
    if not isinstance(input_bytes, str):
        # 如果不是字符串类型，说明是字节串，使用指定的编码进行解码操作，将其转换为字符串
        input_bytes = input_bytes.decode(encoding)
    # 如果是字符串类型或者已经转换为字符串类型后，直接返回
    return input_bytes


def register_view(bp_or_app, routes, view_func, *args, **kwargs):
    """
    用于在蓝图（Blueprint）或应用（Flask应用）上注册视图函数。

    :param bp_or_app: 蓝图对象或者Flask应用对象，这决定了视图函数将被注册到哪里。
    :param routes: 一个包含路由（URL规则）的可迭代对象，例如列表或元组，每个元素是一个路由字符串。
    :param view_func: 要注册的视图函数，即处理对应路由请求的函数。
    :param *args: 传递给add_url_rule方法的位置参数（除了已经在函数签名中明确列出的参数之外的其他位置参数）。
    :param **kwargs: 传递给add_url_rule方法的关键字参数（除了已经在函数签名中明确列出的参数之外的其他关键字参数）。
    """
    for route in routes:
        # 为每个路由在蓝图或应用上添加URL规则
        bp_or_app.add_url_rule(route, view_func=view_func, *args, **kwargs)


def anonymous_required(f):
    """
    这是一个装饰器函数，用于确保被装饰的函数只能由未认证的用户（匿名用户）访问。

    参数：
    f：被装饰的函数

    返回值：
    wrapper函数，它会根据用户的认证状态决定是重定向还是执行被装饰的函数
    """
    @wraps(f)
    def wrapper(*a, **k):
        """
        内部的包装函数，用于检查用户认证状态并执行相应操作。

        参数：
        *a：任意位置参数
        **k：任意关键字参数

        返回值：
        如果用户已认证，重定向到论坛首页；如果用户未认证，执行被装饰的函数并返回其结果
        """
        # 检查当前用户是否存在且已认证
        if current_user is not None and current_user.is_authenticated:
            # 如果用户已认证，重定向到论坛首页
            return redirect_or_next(url_for("forum.index"))
        # 如果用户未认证，执行被装饰的函数
        return f(*a, **k)

    return wrapper


def enforce_recaptcha(limiter):
    """
    此函数用于判断是否需要强制执行reCAPTCHA验证。

    参数:
    limiter: 用于速率限制的对象

    返回值:
    如果需要执行reCAPTCHA验证则返回True，否则返回False
    """
    # 获取当前视图的速率限制（如果有）
    current_limit = getattr(g, "view_rate_limit", None)
    login_recaptcha = False
    if current_limit is not None:
        # 获取窗口统计信息（可能是一个包含开始时间和请求数量的元组）
        window_stats = limiter.limiter.get_window_stats(*current_limit)
        # 计算剩余的允许请求数量与配置中触发reCAPTCHA的阈值的关系
        stats_diff = xfree_config["AUTH_REQUESTS"] - window_stats[1]
        # 如果剩余允许请求数量小于等于触发reCAPTCHA的阈值，则需要执行reCAPTCHA验证
        login_recaptcha = stats_diff >= xfree_config["LOGIN_RECAPTCHA"]
    return login_recaptcha


def registration_enabled(f):
    """
    这是一个装饰器函数，用于检查注册功能是否启用。

    参数：
    f：被装饰的函数

    返回值：
    wrapper函数，它会根据注册功能是否启用决定是重定向还是执行被装饰的函数
    """
    @wraps(f)
    def wrapper(*args, **kwargs):
        """
        内部的包装函数，用于检查注册功能的启用状态并执行相应操作。

        参数：
        *args：任意位置参数
        **kwargs：任意关键字参数

        返回值：
        如果注册功能未启用，显示提示信息并将用户重定向到论坛首页；如果注册功能启用，执行被装饰的函数并返回其结果
        """
        # 判断注册功能是否被禁用
        if not xfree_config["REGISTRATION_ENABLED"]:
            # 如果注册功能被禁用，显示提示信息
            flash("The registration has been disabled.", "info")
            # 将用户重定向到论坛首页
            return redirect_or_next(url_for("forum.index"))
        # 如果注册功能启用，执行被装饰的函数
        return f(*args, **kwargs)

    return wrapper



from functools import wraps
from flask import flash, redirect, url_for


def requires_unactivated(f):
    """
    这是一个装饰器函数。
    它用于判断当前用户的账户是否未激活，只有未激活的用户才能访问被装饰的函数。

    参数：
    f：被装饰的函数

    返回值：
    wrapper函数，根据用户账户激活状态决定是重定向还是执行被装饰的函数
    """
    @wraps(f)
    def wrapper(*args, **kwargs):
        """
        内部的包装函数，进行用户账户激活状态的检查和相应操作。

        参数：
        *args：任意位置参数
        **kwargs：任意关键字参数

        返回值：
        如果用户账户已激活或者不需要激活账户，显示提示信息并将用户重定向到论坛首页；否则执行被装饰的函数
        """
        # 如果当前用户的账户是激活状态，或者配置中不需要激活账户
        if current_user.is_active or not xfree_config["ACTIVATE_ACCOUNT"]:
            # 显示账户已激活的提示信息
            flash("This account is already activated.", "info")
            # 将用户重定向到论坛首页
            return redirect(url_for("forum.index"))
        # 如果用户账户未激活，执行被装饰的函数
        return f(*args, **kwargs)

    return wrapper


def do_topic_action(topics, user, action, reverse):  # noqa: C901
    """对主题执行特定操作，返回修改后的主题对象列表

    :param topics: 可迭代的`Topic`对象
    :param user: 想要执行操作的用户对象
    :param action: 操作类型，如locked（锁定）、important（标记重要）、delete（删除）等
    :param reverse: 是否以相反方式执行操作，例如解锁操作时，`reverse`应设为`True`
    """
    # 如果没有主题，直接返回False
    if not topics:
        return False

    from xfree.utils.requirements import (
        CanDeleteTopic,
        Has,
        IsAtleastModeratorInForum,
    )

    # 检查用户是否至少是主题所在论坛的版主，如果没有权限
    if not Permission(IsAtleastModeratorInForum(forum=topics[0].forum)):
        # 显示权限不足的提示信息
        flash(_("你没有执行此操作的权限。"), "danger")
        return False

    modified_topics = 0
    # 如果操作不是删除、隐藏、取消隐藏
    if action not in {"delete", "hide", "unhide"}:
        for topic in topics:
            # 如果主题的当前操作属性已经为True且不反向操作，则跳过
            if getattr(topic, action) and not reverse:
                continue
            # 设置主题的操作属性为反向操作的结果
            setattr(topic, action, not reverse)
            modified_topics += 1
            # 保存主题的修改
            topic.save()

    # 如果操作是删除
    elif action == "delete":
        # 检查用户是否有删除主题的权限
        if not Permission(CanDeleteTopic):
            # 显示没有删除权限的提示信息
            flash(_("你没有删除这些主题的权限。"), "danger")
            return False
        for topic in topics:
            modified_topics += 1
            # 删除主题
            topic.delete()

    # 如果操作是隐藏
    elif action == "hide":
        # 检查用户是否有隐藏主题的权限
        if not Permission(Has("makehidden")):
            # 显示没有隐藏权限的提示信息
            flash(_("你没有隐藏这些主题的权限。"), "danger")
            return False
        for topic in topics:
            # 如果主题已经隐藏，则跳过
            if topic.hidden:
                continue
            modified_topics += 1
            # 隐藏主题，并传入执行操作的用户
            topic.hide(user)

    # 如果操作是取消隐藏
    elif action == "unhide":
        # 检查用户是否有取消隐藏主题的权限
        if not Permission(Has("makehidden")):
            # 显示没有取消隐藏权限的提示信息
            flash(_("你没有取消隐藏这些主题的权限。"), "danger")
            return False
        for topic in topics:
            # 如果主题没有隐藏，则跳过
            if not topic.hidden:
                continue
            modified_topics += 1
            # 取消隐藏主题
            topic.unhide()

    return modified_topics


def format_quote(username, content):
    """
    根据标记语言（markup language）格式化引用内容。它会生成一个包含用户名、用户资料链接和引用内容的格式化字符串。
    参数：
    - username (str): 用户名。
    - content (str): 引用内容。
    返回：
    - str: 格式化后的引用内容字符串。
    """
    # 根据用户名生成用户资料页面的链接
    profile_url = url_for("user.profile", username=username)
    # 对引用内容进行处理，先去除两端空白字符，然后按换行符分割成列表
    # 再用"\n> "将列表元素重新连接，这样每行内容前会加上"> "
    content = "\n> ".join(content.strip().split("\n"))
    # 按照特定格式生成引用字符串，包括加粗的用户名链接、" wrote:"字样和处理后的引用内容
    quote = "**[{username}]({profile_url}) wrote:**\n> {content}\n".format(
        username=username, profile_url=profile_url, content=content
    )
    return quote


def real(obj):
    """
    该函数用于解包（unwrap）一个werkzeug.local.LocalProxy对象，如果传入的是该对象的话；
    如果传入的不是LocalProxy对象，则直接返回该对象。

    :param obj: 要处理的对象
    """
    # 判断传入的对象是否是LocalProxy类型
    if isinstance(obj, LocalProxy):
        # 如果是，返回其当前对象。这里通过访问LocalProxy对象的_get_current_object()方法来获取其实际对象
        return obj._get_current_object()
    # 如果不是LocalProxy对象，直接返回传入的对象
    return obj